/*
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.fabric8.zjsonpatch;

import com.fasterxml.jackson.databind.JsonNode;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class is ported from <a href=
 * "https://github.com/flipkart-incubator/zjsonpatch/blob/a446bf598231c06006d4e3df69b846cdb16d8889/src/main/java/com/flipkart/zjsonpatch/JsonPointer">FlipKart
 * zjsonpatch repository</a>
 */
public class JsonPointer {
  private final RefToken[] tokens;

  /** A JSON pointer representing the root node of a JSON document */
  public final static JsonPointer ROOT = new JsonPointer(new RefToken[] {});

  private JsonPointer(RefToken[] tokens) {
    this.tokens = tokens;
  }

  /**
   * Constructs a new pointer from a list of reference tokens.
   *
   * @param tokens The list of reference tokens from which to construct the new pointer. This list is not modified.
   */
  public JsonPointer(List<RefToken> tokens) {
    this.tokens = tokens.toArray(new RefToken[0]);
  }

  /**
   * Parses a valid string representation of a JSON Pointer.
   *
   * @param path The string representation to be parsed.
   * @return An instance of {@link JsonPointer} conforming to the specified string representation.
   * @throws IllegalArgumentException The specified JSON Pointer is invalid.
   */
  public static JsonPointer parse(String path) throws IllegalArgumentException {
    StringBuilder reftoken = null;
    List<RefToken> result = new ArrayList<>();

    for (int i = 0; i < path.length(); ++i) {
      char c = path.charAt(i);

      // Require leading slash
      if (i == 0) {
        if (c != '/')
          throw new IllegalArgumentException("Missing leading slash");
        reftoken = new StringBuilder();
        continue;
      }

      switch (c) {
        // Escape sequences
        case '~':
          switch (path.charAt(++i)) {
            case '0':
              reftoken.append('~');
              break;
            case '1':
              reftoken.append('/');
              break;
            default:
              throw new IllegalArgumentException("Invalid escape sequence ~" + path.charAt(i) + " at index " + i);
          }
          break;

        // New reftoken
        case '/':
          result.add(new RefToken(reftoken.toString()));
          reftoken.setLength(0);
          break;

        default:
          reftoken.append(c);
          break;
      }
    }

    if (reftoken == null)
      return ROOT;

    result.add(RefToken.parse(reftoken.toString()));
    return new JsonPointer(result);
  }

  /**
   * Indicates whether or not this instance points to the root of a JSON document.
   *
   * @return {@code true} if this pointer represents the root node, {@code false} otherwise.
   */
  public boolean isRoot() {
    return tokens.length == 0;
  }

  /**
   * Creates a new JSON pointer to the specified field of the object referenced by this instance.
   *
   * @param field The desired field name, or any valid JSON Pointer reference token
   * @return The new {@link JsonPointer} instance.
   */
  JsonPointer append(String field) {
    RefToken[] newTokens = Arrays.copyOf(tokens, tokens.length + 1);
    newTokens[tokens.length] = new RefToken(field);
    return new JsonPointer(newTokens);
  }

  /**
   * Creates a new JSON pointer to an indexed element of the array referenced by this instance.
   *
   * @param index The desired index, or {@link #LAST_INDEX} to point past the end of the array.
   * @return The new {@link JsonPointer} instance.
   */
  JsonPointer append(int index) {
    return append(Integer.toString(index));
  }

  /** Returns the number of reference tokens comprising this instance. */
  int size() {
    return tokens.length;
  }

  /**
   * Returns a string representation of this instance
   *
   * @return
   *         An <a href="https://tools.ietf.org/html/rfc6901#section-5">RFC 6901 compliant</a> string
   *         representation of this JSON pointer.
   */
  public String toString() {
    StringBuilder sb = new StringBuilder();
    for (RefToken token : tokens) {
      sb.append('/');
      sb.append(token);
    }
    return sb.toString();
  }

  /**
   * Decomposes this JSON pointer into its reference tokens.
   *
   * @return A list of {@link RefToken}s. Modifications to this list do not affect this instance.
   */
  public List<RefToken> decompose() {
    return Arrays.asList(tokens.clone());
  }

  /**
   * Retrieves the reference token at the specified index.
   *
   * @param index The desired reference token index.
   * @return The specified instance of {@link RefToken}.
   * @throws IndexOutOfBoundsException The specified index is illegal.
   */
  public RefToken get(int index) throws IndexOutOfBoundsException {
    if (index < 0 || index >= tokens.length)
      throw new IndexOutOfBoundsException("Illegal index: " + index);
    return tokens[index];
  }

  /**
   * Retrieves the last reference token for this JSON pointer.
   *
   * @return The last {@link RefToken} comprising this instance.
   * @throws IllegalStateException Last cannot be called on {@link #ROOT root} pointers.
   */
  public RefToken last() {
    if (isRoot())
      throw new IllegalStateException("Root pointers contain no reference tokens");
    return tokens[tokens.length - 1];
  }

  /**
   * Creates a JSON pointer to the parent of the node represented by this instance.
   *
   * The parent of the {@link #ROOT root} pointer is the root pointer itself.
   *
   * @return A {@link JsonPointer} to the parent node.
   */
  public JsonPointer getParent() {
    return isRoot() ? this : new JsonPointer(Arrays.copyOf(tokens, tokens.length - 1));
  }

  private void error(int atToken, String message, JsonNode document) throws JsonPointerEvaluationException {
    throw new JsonPointerEvaluationException(
        message,
        new JsonPointer(Arrays.copyOf(tokens, atToken)),
        document);
  }

  /**
   * Takes a target document and resolves the node represented by this instance.
   *
   * The evaluation semantics are described in
   * <a href="https://tools.ietf.org/html/rfc6901#section-4">RFC 6901 sectino 4</a>.
   *
   * @param document The target document against which to evaluate the JSON pointer.
   * @return The {@link JsonNode} resolved by evaluating this JSON pointer.
   * @throws JsonPointerEvaluationException The pointer could not be evaluated.
   */
  public JsonNode evaluate(final JsonNode document) throws JsonPointerEvaluationException {
    JsonNode current = document;

    for (int idx = 0; idx < tokens.length; ++idx) {
      final RefToken token = tokens[idx];

      if (current.isArray()) {
        if (!token.isArrayIndex())
          error(idx, "Can't reference field \"" + token.getField() + "\" on array", document);
        if (token.getIndex() == LAST_INDEX || token.getIndex() >= current.size())
          error(idx, "Array index " + token.toString() + " is out of bounds", document);
        current = current.get(token.getIndex());
      } else if (current.isObject()) {
        if (!current.has(token.getField()))
          error(idx, "Missing field \"" + token.getField() + "\"", document);
        current = current.get(token.getField());
      } else
        error(idx, "Can't reference past scalar value", document);
    }

    return current;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o)
      return true;
    if (o == null || getClass() != o.getClass())
      return false;

    JsonPointer that = (JsonPointer) o;

    // Probably incorrect - comparing Object[] arrays with Arrays.equals
    return Arrays.equals(tokens, that.tokens);
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(tokens);
  }

  /** Represents a single JSON Pointer reference token. */
  static class RefToken {
    private String decodedToken;
    transient private Integer index = null;

    public RefToken(String decodedToken) {
      if (decodedToken == null)
        throw new IllegalArgumentException("Token can't be null");
      this.decodedToken = decodedToken;
    }

    private static final Pattern DECODED_TILDA_PATTERN = Pattern.compile("~0");
    private static final Pattern DECODED_SLASH_PATTERN = Pattern.compile("~1");

    private static String decodePath(Object object) {
      String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4
      path = DECODED_SLASH_PATTERN.matcher(path).replaceAll("/");
      return DECODED_TILDA_PATTERN.matcher(path).replaceAll("~");
    }

    private static final Pattern ENCODED_TILDA_PATTERN = Pattern.compile("~");
    private static final Pattern ENCODED_SLASH_PATTERN = Pattern.compile("/");

    private static String encodePath(Object object) {
      String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4
      path = ENCODED_TILDA_PATTERN.matcher(path).replaceAll("~0");
      return ENCODED_SLASH_PATTERN.matcher(path).replaceAll("~1");
    }

    private static final Pattern VALID_ARRAY_IND = Pattern.compile("-|0|(?:[1-9][0-9]*)");

    public static RefToken parse(String rawToken) {
      if (rawToken == null)
        throw new IllegalArgumentException("Token can't be null");
      return new RefToken(decodePath(rawToken));
    }

    public boolean isArrayIndex() {
      if (index != null)
        return true;
      Matcher matcher = VALID_ARRAY_IND.matcher(decodedToken);
      if (matcher.matches()) {
        index = matcher.group().equals("-") ? LAST_INDEX : Integer.parseInt(matcher.group());
        return true;
      }
      return false;
    }

    public int getIndex() {
      if (!isArrayIndex())
        throw new IllegalStateException("Object operation on array target");
      return index;
    }

    public String getField() {
      return decodedToken;
    }

    @Override
    public String toString() {
      return encodePath(decodedToken);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o)
        return true;
      if (o == null || getClass() != o.getClass())
        return false;

      RefToken refToken = (RefToken) o;

      return decodedToken.equals(refToken.decodedToken);
    }

    @Override
    public int hashCode() {
      return decodedToken.hashCode();
    }
  }

  /**
   * Represents an array index pointing past the end of the array.
   *
   * Such an index is represented by the JSON pointer reference token "{@code -}"; see
   * <a href="https://tools.ietf.org/html/rfc6901#section-4">RFC 6901 section 4</a> for
   * more details.
   */
  final static int LAST_INDEX = Integer.MIN_VALUE;
}
